/*
 * 代号：凤凰
 * 2014年6月30日
 * V3.0
 */
package com.jphenix.servlet.filter;

//#region 【引用区】
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import com.jphenix.servlet.parent.ServiceBeanParent;
import com.jphenix.share.lang.SBoolean;
import com.jphenix.share.lang.SString;
import com.jphenix.share.util.BaseUtil;
import com.jphenix.share.util.BytesArrayUtil;
import com.jphenix.standard.docs.BeanInfo;
import com.jphenix.standard.docs.ClassInfo;
import com.jphenix.standard.docs.Register;
import com.jphenix.standard.docs.Running;
import com.jphenix.standard.servlet.IFilter;
import com.jphenix.standard.servlet.IRequestManager;
import com.jphenix.standard.servlet.IResponseManager;
import com.jphenix.standard.servlet.api.IFilterConfig;
import com.jphenix.standard.servlet.api.IRequest;
import com.jphenix.standard.servlet.api.IResponse;
import com.jphenix.standard.viewhandler.IViewHandler;
//#endregion

//#region 【说明区】
/**
 * 页面内容过滤器
 * 代理全部访问请求，并修改内部HTML代码
 * 
 * <page_proxy_url>http://localhost/contact</page_proxy_url> <!-- 目标根路径 -->
 * <page_proxy_replace>            <!-- 调用被代理请求后需要处理返回内容(替换指定字符) -->
 *   <sub match="/b.html">         <!-- 指定请求路径 为空时为全部请求路径 -->
 *     <key>content="IE=5"</key>   <!-- 需要替换的值 -->
 *     <val>content="IE=7"</val>   <!-- 替换后的值 -->
 *   </sub>
 *   <sub match="/b.html">
 *     <key>:9081</key>
 *     <val>:9080</val>
 *   </sub>
 * </page_proxy_replace>
 * 
 * 
 * 2019-04-16 修改了遇到的错误
 * 2019-06-15 按照IFilter增加了过滤器初始化方法
 * 2019-07-03 设置了代理标识，客户端真实地址
 * 2019-08-19 优化了流，增加了缓存
 * 2022-09-04 隔离了ServletApi，兼容新老Tomcat
 * 
 * @author MBG
 */
//#endregion
@ClassInfo({"2024-07-14 13:39","页面内容过滤器"})
@BeanInfo({"pageproxyfilter"})
@Register({"filtervector"})
@Running({"1"})
public class PageProxy extends ServiceBeanParent implements IFilter {

	//#region 【声明区】
	private int        outTime      = 300000;   // 超时时间（5分钟）
	private String     objUrl       = null;     // 目标访问URL
	private String[]   matchKeys    = null;     // url匹配
	private byte[][]   fromKeyBytes = null;     // 目标被替换字符串字节数组
	private byte[][]   objKeyBytes  = null;     // 需要替换成字符串的字节数组
	//#endregion
	
	//#region init(bf,config) 执行初始化bb
	/**
	 * 执行初始化
	 * @param bf         过滤器管理类
	 * @param config     Servlet配置信息类
	 * @throws Exception 异常（如果初始化发生异常，则放弃不再使用）
	 * 2019年6月15日
	 * @author MBG
	 */
	@Override
	public void init(BaseFilter bf, IFilterConfig config) throws Exception {
		objUrl       = p("page_proxy_url");
		//获取替换段
		IViewHandler subVh = px("page_proxy_replace");
		//替换信息段序列
		List<IViewHandler> eleList = subVh.cnn("sub");
		fromKeyBytes = new byte[eleList.size()][];
		objKeyBytes  = new byte[eleList.size()][];
		matchKeys    = new String[eleList.size()];
		IViewHandler ele; //信息元素
		for(int i=0;i<eleList.size();i++) {
			ele             = eleList.get(i);
			matchKeys[i]    = ele.a("match");
			fromKeyBytes[i] = ele.fnn("key").nt().getBytes();
			objKeyBytes[i]  = ele.fnn("val").nt().getBytes();
		}
		
		//设置允许修改Host头值
		System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
		
		log.startLog("\n--------------------------------------------------------------------\n"
	    		  +"***PageProxy Has Started\n"
	    		  +"--------------------------------------------------------------------\n\n");
	}
	//#endregion
	
	//#region getIndex() 获取排序索引，数值越小越先执行
	/**
	 * 覆盖方法
	 */
	@Override
	public int getIndex() {
		return 1;
	}
	//#endregion
	
	//#region getFilterActionExtName() 获取需要过滤的动作路径扩展名
	/**
	 * 覆盖方法
	 */
	@Override
	public String getFilterActionExtName() {
		return "*";
	}
	//#endregion
	
	//#region doFilter(req,resp) 执行过滤器
	/**
	 * 覆盖方法
	 */
	@Override
	public boolean doFilter(IRequestManager req, IResponseManager resp) throws Exception {
        String toUrl = objUrl; //目标URL
        //动作路径（不带虚拟路径）
        String sPath = req.getServletPath();
        if(sPath!=null) {
        	toUrl+=sPath;
        }
        //提交参数
        String queryStr = req.getQueryString();
        if(queryStr!=null && queryStr.length()>0) {
        	toUrl += "?"+queryStr;
        }
        //输出流缓存
    	ByteArrayOutputStream bos = new ByteArrayOutputStream();
    	//获取输出流
    	OutputStream respOs = resp.iGetOutputStream();
    	//执行代理调用
    	call(toUrl,outTime,req,resp,respOs,bos);
    	
    	if(bos.size()>0) {
	    	byte[] res = bos.toByteArray(); //返回值数组
	    	for(int i=0;i<fromKeyBytes.length;i++) {
	    		if(matchKeys[i].length()<1 || sPath.indexOf(matchKeys[i])>-1) {
	    			res = BytesArrayUtil.arrayReplace(res,fromKeyBytes[i],objKeyBytes[i]);
	    		}
	    	}
	    	respOs.write(res);
    	}
        return true;
	}
	//#endregion
	
	//#region 【内部方法】call(sUrl,outTime,req,resp,respOs,dataOs) 调用URL并直接输出
    /**
     * 调用URL并直接输出
     * @param sUrl      调用url
     * @param outTime   超时时间
     * @param req       请求对象
     * @param resp      反馈对象
     * @param respOs    反馈的数据流
     * @param dataOs    输出流
     * @throws Exception 异常
     * 2015年11月16日
     * @author 马宝刚
     */
	private void call(
            String        sUrl
            ,int          outTime
            ,IRequest     req
            ,IResponse    resp
            ,OutputStream respOs
            ,OutputStream dataOs) throws Exception {
        int         rCount;                      //读取字节数
        byte[]      buffer     = new byte[2048]; //读入缓存
        InputStream is         = null;           //读入信息流
        int         statusCode = 0;              //返回状态码
        String      statusMsg  = "";             //返回状态信息
        InputStream postIs     = null;           //提交数据读入流
        if(!SBoolean.valueOf(req.iGetSession().getAttribute("DO_AUTH"))) {
        	req.iGetSession().setAttribute("DO_AUTH","1");
        	resp.sendError(401);
        	resp.setHeader("WWW-Authenticate","NTLM");
        	return;
        }
        //如果是文件下载，直接用resp的输出流，否则用字节缓存流
        //然后做批量替换虚拟路径
        OutputStream callOs = null; //调用后返回流
        try {
            //构造URL
            URL url = new URL(sUrl);
            //构造链接
            URLConnection urlc = url.openConnection();
            if(outTime>0) {
                urlc.setConnectTimeout(outTime);
                urlc.setReadTimeout(outTime);
            }
            if(urlc instanceof HttpsURLConnection) {
                //通常调用的路径都是内部指定的，非得验证域名，导致域名验证失败
                ((HttpsURLConnection)urlc).setHostnameVerifier(new HostnameVerifier() {
                    @Override
                    public boolean verify(String hostname, SSLSession session) {
                        return true;
                    }
                });
                ((HttpsURLConnection)urlc).setSSLSocketFactory(createSSLContext().getSocketFactory());
            }
            //设置不自动跳转获取数据（因为自动跳转时不会自动将cookie信息带过去，会导致session丢失）
            ((HttpURLConnection)urlc).setInstanceFollowRedirects(false);
            
            //头信息容器
            Map<String, String> headerMap = req.getHeaderMap();
            //设置头信息
            String[] keyList = BaseUtil.getMapKeys(headerMap);
            String value; //头信息值
            for(String key:keyList) {
                value = SString.valueOf(headerMap.get(key));
                if(value.length()<1) {
                    continue;
                }
                urlc.setRequestProperty(key,value);
            }
            
            //设置代理标识，客户端真实地址
            urlc.setRequestProperty("X-Forwarded-For",req.getRemoteAddr());
            
            urlc.setDoOutput(true);   
            urlc.setUseCaches(false);
            if(!"GET".equalsIgnoreCase(req.getMethod())) {
            	//获取提交信息读入流
            	postIs = req.iGetInputStream();
            	if(postIs!=null) {
                    //构建输出信息流
                    OutputStream out = urlc.getOutputStream();
                    while((rCount=postIs.read(buffer))!=-1) {
                        out.write(buffer,0,rCount);
                    }
            	}
            }
            statusCode = ((HttpURLConnection)urlc).getResponseCode();
            statusMsg = ((HttpURLConnection)urlc).getResponseMessage();
            resp.sendError(statusCode,statusMsg);
          
            //处理返回头信息
            Iterator<String> keys = urlc.getHeaderFields().keySet().iterator();
            String key;
            while(keys.hasNext()) {
            	key   = SString.valueOf(keys.next());
            	if(key.length()<1) {
            		continue;
            	}
            	value = SString.valueOf(urlc.getHeaderField(key));
            	resp.setHeader(key,value);
            }
            if(statusCode==HttpURLConnection.HTTP_MOVED_TEMP || statusCode==HttpURLConnection.HTTP_MOVED_PERM) {
            	//重定向URL
            	String goUrl = SString.valueOf(urlc.getHeaderField("Location"));
            	if(goUrl.length()>0) {
            		//代理服务器的根URL
            		String proxyUrl = req.getRequestURL().toString();
            		int point = proxyUrl.indexOf("/",9);
            		if(point>0) {
            			proxyUrl = proxyUrl.substring(0,point);
            		}
            		if(goUrl.indexOf("://")<0) {
            			proxyUrl = proxyUrl+goUrl;
            		}else {
            			point = goUrl.indexOf("/",9);
            			if(point>0) {
            				proxyUrl = proxyUrl+goUrl.substring(point);
            			}
            		}
            		log("---["+statusCode+"]:["+sUrl+"]  Redirect:["+proxyUrl+"]");
            		resp.setHeader("Location",proxyUrl);
            	}
            }else {
            	log("---["+statusCode+"]:["+sUrl+"]");
            }
            if(statusCode==HttpURLConnection.HTTP_OK){
            	callOs = dataOs==null?respOs:dataOs;
                //获取信息流
                is = urlc.getInputStream();
                while ((rCount=is.read(buffer))!=-1) {
                	callOs.write(buffer,0,rCount);
                }
            }
        } catch (IOException e) {
            //e.printStackTrace();
            //构建错误信息
            String msg = "HttpCall Exception URL:["+sUrl+"] Params:[InputStream] e:["
                    +e+"] HTTP Status:["+statusCode+"] Msg:["+statusMsg+"]";
            System.err.print(msg);
            return;
            //已经往resp中设置了状态码和错误信息
            //throw new MsgException(HttpCall.class,msg);
        }finally {
        	if(postIs!=null) {
                try {
                	postIs.close();
                } catch (Exception e2) {}
        	}
            try {
                is.close();
            } catch (Exception e2) {}
            try {
            	callOs.flush();
            }catch(Exception e2) {}
        }
    }
    //#endregion
    
	//#region createSSLContext() 构建无需私钥的上下文对象
	/**
	 * 构建无需私钥的上下文对象
	 * HttpCall 中也有这个方法，但是为了效率，还是在当前类中弄一个吧
	 * @return 上下文对象
	 * 2015年1月20日
	 * @author 马宝刚
	 */
    private SSLContext createSSLContext() {
        SSLContext sslcontext = null; //构建返回值
        try {
             sslcontext = SSLContext.getInstance("TLS");
             sslcontext.init(null, new TrustManager[] { new X509TrustManager() {
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
                @Override
                public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {}
                @Override
                public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {}
            }}, null);
        }catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
       } catch (KeyManagementException e) {
           e.printStackTrace();
      }
        SSLContext.setDefault(sslcontext);
        return sslcontext;
    }
    //#endregion
    
    //#region destroy() 销毁当前过滤器
    /**
     * 覆盖方法
     */
    @Override
    public void destroy() {}
    //#endregion
}









